Sync 与 Async Python:有什么区别?
你有没有听人说过异步 Python 代码比“普通”(或同步) Python 代码更快?这怎么可能?在本文中,我将尝试解释什么是异步以及它与普通 Python 代码的区别。
Sync 和 Async 是什么意思?
Web 应用程序通常需要处理许多请求,所有请求都是在短时间内从不同的客户端发出的。为了避免处理延迟,它们必须能够并行处理多个请求(通常称为并发)。在本文中,我将继续使用 web 应用程序作为例子,但请记住,还有其他类型的应用程序也受益于同时完成多个任务,因此这个讨论并不是专门针对 web 的。
术语“ sync”和“ async”指的是编写使用并发的应用程序的两种方式。所谓的“sync”服务器使用线程和进程的底层操作系统支持来实现这种并发。以下是同步部署的效果图:
在这种情况下,我们有五个客户端,所有客户端都向应用程序发送请求。这个应用程序的公共访问点是一个 web 服务器,它充当一个负载均衡器,将请求分发给一组服务器 worker,这些 worker 可以实现为进程、线程或两者的组合。worker 执行负载均衡器分配给他们的请求。你可以用 Flask 或 Django 这样的 web 应用程序框架来编写应用程序逻辑,它们就位于这些 worker 中。
这种类型的解决方案非常适合拥有多个 CPU 的服务器,因为你可以将 worker 的数量配置为 cpu 数量的倍数,并且通过这种配置,实现 cores 的均匀利用率,这是单个 Python 进程无法做到的,因为 全局解释器锁定(GIL) 强加了一些限制。
就缺点而言,上图清楚地表明了此方法的主要局限性。 我们有5个客户端,但只有4个 worker 。 如果这5个客户端同时发送请求,而负载平衡器只能给每个worker 分派 1个请求,没有竞争到 worker 的请求将保留在队列中,等待一个 worker 可用。 因此,5个客户端中有4个将及时收到回复,但其中1个将不得不等待更长的时间。 使服务器性能良好的关键在于选择适当数量的 worker,以防止或尽量减少在给定预期负载的情况下阻塞请求的情况。
异步服务器设置较难绘制,但这是我的最佳选择:
这种类型的服务器在由循环控制的单个进程中运行。 循环是一个非常高效的任务管理器和调度器,它创建任务以处理客户端发送的请求。 与长期运行的服务器 worker 不同,循环会创建一个异步任务来处理特定的请求,当该请求完成时,该任务将被销毁。 在任何给定的时间,异步服务器可能有数百个甚至数千个活动任务,所有这些任务由循环管理并同时完成自己的工作。
你可能想知道异步任务之间的并行性是如何实现的。这是有趣的部分,因为异步应用程序完全依赖于协作式多任务处理。这意味着什么?当一个任务需要等待一个外部事件时,例如来自数据库服务器的响应,而不是像同步 worker 那样等待,它告诉循环需要等待什么,然后将控制权返回给循环。然后循环可以找到另一个准备运行的任务,而此任务被数据库阻塞。最终数据库将发送响应,此时循环将考虑第一个任务准备再次运行,并将尽快恢复它。
异步任务暂停和恢复执行的这种能力比较抽象可能难以理解。为了帮助你将其应用于你可能已经知道的事情,请考虑在 Python 中,实现此目的的一种方法是使用 await
或 yield
关键字,但这并不是唯一的方法,你稍后将看到。
异步应用程序完全在单个进程和单个线程中运行,这令人惊讶。 当然,这种类型的并发需要一定的规则,因为你不能让任务在 CPU 上停留太长时间,否则剩余的任务就会饿死。 为了使异步工作,所有任务都需要自动暂停并及时将控制权返回给循环。 要从异步风格中受益,应用程序需要执行的任务通常会被 I/O 阻塞,并且不需要太多的CPU工作。 Web应用程序通常非常适合,特别是如果它们需要处理大量客户端请求时。
为了在使用异步服务器时最大限度地利用多个 CPU,通常会创建一个混合解决方案,添加一个负载平衡器并在每个CPU上运行一个异步服务器,如下图所示:
在 Python 中实现异步的两种方法
我确定你知道,要在 Python 中编写异步应用程序,你可以使用 asyncio 包,它构建在协程之上,以实现所有异步应用程序都需要的挂起和恢复特性。关键词 yield
,以及更新的 async
和 await
,是 asyncio 构建异步功能的基础。为了描绘一幅完整的图景,Python 生态系统中还有其他基于协程的异步解决方案,比如 Trio 和 Curio。还有 Twisted,它是最古老的协同框架,甚至早于 asyncio
。
如果你有兴趣编写一个异步 web 应用程序,有很多基于 coroutines 的异步框架可供选择,包括 aiohttp、sanic、FastAPI 和 Tornado。
很多人不知道的是,协程只是 Python 中可用于编写异步代码的两种方法之一。 第二种方法是基于一个名为greenlet 的软件包,你可以使用pip进行安装。 Greenlets与协程类似,因为它们也允许 Python 函数暂停执行并在以后恢复执行,但是实现方式却完全不同,这意味着 Python 中的异步生态系统分为两个大类。
coroutine 和 greenlets 进行异步开发的有趣的区别在于,前者需要 Python 语言的特定关键字和特性才能工作,而后者不需要。我的意思是,基于 coroutine 的应用程序需要使用非常特定的语法编写,而基于 greenlet 的应用程序看起来与普通的 Python 代码完全一样。这非常酷,因为在某些条件下,它允许异步执行同步代码,这是基于 coroutine 的解决方案(例如asyncio)无法做到的。
那么在 greenlet 方面有哪些 asyncio
的等价物呢?我知道三个基于 greenlets 的异步包: Gevent, Eventlet 和 Meinheld,尽管最后一个更像是一个 web 服务器而不是一个通用的异步库。它们都有自己的异步循环实现,并且提供了一个有趣的“ monkey-patching”特性,用在 greenlets 上实现的等效非阻塞版本替换 Python 标准库中的阻塞函数,比如那些执行网络和线程的函数。如果你有一段希望异步运行的同步代码,那么这些包很有可能会让你做到这一点。
你会对此感到惊讶的。据我所知,唯一明确支持 greenlets 的 web 框架是 Flask。此框架会自动检测你何时在greenlet web 服务器上运行,并进行相应的调整,而无需进行任何配置。在执行此操作时,您需要注意不要调用阻塞函数,否则,请使用Monkey-patching来“修复”这些阻塞函数。
但是,Flask 并不是唯一可以从 greenlets 中受益的框架。 其他的 web 框架,比如 Django 和 Bottle,它们不知道 greenlets,当它们与 greenlet web 服务器配对时,也可以异步运行,并且猴子补丁修复了阻塞功能。
异步比同步更快吗?
关于同步和异步应用程序的性能,存在广泛的误解。 人们认为异步应用程序比同步应用程序要快得多。
让我澄清一下,以便我们达成统一认知。 不管 Python 代码是同步编写还是异步编写,它的运行速度都是完全相同的。 除了代码外,还有两个因素可以影响并发应用程序的性能:上下文切换和可伸缩性。
上下文切换
在所有正在运行的任务之间公平地共享 CPU 所需的工作(称为上下文切换)可能会影响应用程序的性能。对于同步应用程序,此工作由操作系统完成,并且基本上是一个没有配置或微调选项的黑匣子。对于异步应用程序,上下文切换由循环完成。
asyncio 提供的默认循环实现是用 Python 编写的,它不被认为是非常高效的。 uvloop
软件包提供了一个替代循环,该循环部分用C代码实现,以实现更好的性能。 Gevent
和 Meinheld
使用的事件循环也用 C 代码编写。Eventlet
使用 Python 编写的循环。
高度优化的异步循环在进行上下文切换方面可能比操作系统更有效,但是以我的经验,要想看到切实的性能提升,你必须在很高的并发级别上运行。对于大多数应用程序,我认为同步和异步上下文切换之间的性能差异不会很显著。
伸缩性
我认为,异步更快的神话来源于异步应用程序通常能够更有效地使用 cpu,因为它们比同步具有更好的伸缩性和更灵活的方法。
考虑一下如果上图所示的同步服务器同时接收100个请求,将会发生什么情况。该服务器一次不能处理超过4个请求,因此其中大多数请求将在队列中等待一段时间,然后才能分配 worker。
与异步服务器相比,异步服务器会立即创建100个任务(如果使用混合模型,4个异步 worker 每个会创建25个任务)。使用异步服务器,所有请求都可以在不必等待的情况下开始处理(不过公平地说,可能还存在其他会降低速度的瓶颈,比如对活动数据库连接数的限制)。
如果这100个任务大量使用 CPU,那么同步和异步解决方案将有类似的性能,因为 CPU 运行的速度是固定的,Python 的执行代码的速度总是相同的,应用程序所做的工作也是相同的。但是,如果任务需要执行大量 I/O 操作,那么只有4个并发请求,同步服务器可能无法实现高 CPU 利用率。另一方面,异步服务器肯定能够更好地保持 cpu 处于忙碌状态,因为它并行地运行所有100个请求。
你可能想知道为什么不能运行100个同步 worker,这样两个服务器就具有相同的并发性。考虑一下,每个 worker都需要有自己的 Python 解释器,以及与之相关的所有资源,再加上具有自己资源应用程序的一个单独副本。服务器和应用程序的大小将决定可以运行多少个 worker 实例,但通常这个数字并不是很高。另一方面,异步任务非常轻量级,并且都在单个 worker 进程的上下文中运行,因此它们具有明显的优势。
记住这些,我们可以说异步只有在以下情况下才会比同步更快:
- 高负载(没有高负载就没有高并发性的优势)
- 任务受 I/O 约束(如果任务受CPU约束,那么 超过CPU 数以上的并发性就没有帮助了)
- 你可以查看每单位时间处理的平均请求数。如果查看单个请求处理时间,你不会看到很大的差异,并且因为有更多的并发任务竞争 CPU,异步甚至可能会稍微慢一点
我希望本文能够澄清一些关于异步代码的混淆和误解。我希望你们记住以下两个要点:
- 在高负载下,异步应用程序只会比同步等效程序做得更好
- 由于 greenlet,即使你编写普通代码并使用传统框架(如 Flask 或 Django) ,也可以从异步中受益
如果你想更详细地了解异步系统是如何工作的,请查看我的 PyCon 演示文稿 Asynchronous Python for the Complete Beginner。